<?php
// +---------------------------------------------------------[BSD License]--+
//
// Copyright (C) 2005, anatoly techtonik. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
//    notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.
// 
// THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
// 
// +-----------------------------------------------------------------------+
// | Authors: techtonik <techtonik@php.net>                                |
// +-----------------------------------------------------------------------+


require_once "PEAR.php";
require_once "Socket_save.php";

/**
 * Standalone PHP CVS client
 *
 * Provides a very limited readonly implementation of the CVS protocol using
 * PEAR's Net_Socket:: class. Based on version 1.12.9 of the protocol
 * specification.
 *
 * Implemented functions
 * - pserver authenfication
 * - list server commands
 *
 * @package Net_CVS
 * @license BSD
 * @version $Revision: 1.3 $
 * @author  techtonik <techtonik@php.net>
 */
class Net_CVS extends PEAR {

    /**
     * Server version
     * @var string
     */
    var $server_version;
    /**
     * Requests supported by server
     * @var array
     * @access public
     */
    var $server_requests;
    /**
     * Instance of Net_Socket class used to connect to server
     * @var resource
     * @access private
     */
    var $_socket;
    /**
     * CVSROOT path of remote repository
     * @var string
     * @access private
     */
    var $_cvsroot;
    /**
     * Protocol commands supported by this client
     * @var array
     * @access private
     */
    var $_net_cvs_requests = array(
        "UseUnchanged","valid-requests","Valid-responses"
    );
    /**
     * Responses supported by this client
     * @var array
     * @access private
     */
    var $_net_cvs_responses = array(
        "error","ok","Valid-requests"
    );

    /**
     * Constructor.
     * Use Net_CVS::connect() method to get an instance of the class faster.
     *
     * @return void
     */
    function Net_CVS() {
        $this->PEAR();
        $this->setErrorHandling(PEAR_ERROR_DIE);
    }

    /**
     * Make connection to CVS server. Takes a CVS data source name
     * (DSN or so-called CVSROOT) in the form:
     *
     * :protocol:[protocol options]:/path
     *
     * For specific protocol options see description of appropriate
     * _connect_[protocol] method. Where protocol is one of the supported
     * authentication protocols in a class description.
     *
     * @param string CVSROOT DSN - just a compicated name for a simple string
     *               see docs on _connect_... methods for exact format
     * @param bool   just setup object (used for PHPUnit tests)
     *
     * @return reference to Net_CVS object or FALSE on error
     * @access public
     */
    function &connect($cvs_dsn = NULL, $connect = true) {

        if (!isset($this) || !is_a($this, "Net_CVS")) {
            $obj = &new Net_CVS();
        } else {
            $obj = &$this;
        }

        // matches array upon successfull match
        // 1st component - protocol
        // 2nd component - protocol options with necessary :
        // 3rd component - CVSROOT path
        $dsn_regexp = '!^:(\w+)(.*:.*)(/\w[/\w]*)$!';
        if (!preg_match($dsn_regexp, $cvs_dsn, $matches)) {
            $obj->throwError('Invalid DSN (CVSROOT)');
            return FALSE;
        }
        $auth_method   = $matches[1];
        $auth_params   = $matches[2];
        $obj->_cvsroot = $matches[3];

        if (!method_exists($obj, "auth_".$auth_method)) {
            $obj->throwError("Unsupported authentication method '$auth_method'");
            return FALSE;
        }

        // call auth_pserver or other auth_...
        $res = $obj->{"auth_".$auth_method}( $auth_params );
        if (PEAR::isError($res)) {
            return FALSE;
        }

        // proceed with init
        $obj->_init();

        return $obj;
    }

    /**
     * Proceed with connect and pserver authentication using part of DSN,
     * provided by connect method in the following format
     *
     * :[username[:password]@]host[:port][:]
     *
     * @param string Auth
     * @access private
     */
    function auth_pserver($auth_params) {
        // matches array upon successfull match
        // 2nd component - username
        // 4th component - password
        // 5th component - host
        // 7th component - port
        $ps_param_regexp = "!^:((\w+)(:(\w+))?@)?([\w.]+)(:(\d+))?:?$!";
        if (!preg_match($ps_param_regexp, $auth_params, $matches)) {
            $this->throwError('Invalid DSN (pserver authentication parameters)');
            return FALSE;
        }
        $user = $matches[2];
        $pass = $this->scramble_pass( $matches[4] );
        $host = $matches[5];
        $port = (isset($matches[7]))?$matches[7]:2401;

        $result = $this->_connect($host, $port);

        if (PEAR::isError($result)) {
            return $result;
        }
        $cvsroot     = $this->_cvsroot;

        $auth_block  = "BEGIN AUTH REQUEST\n$cvsroot\n$user\n";
        $auth_block .= "A$pass\nEND AUTH REQUEST\n";

        $response = $this->_send_wait_reply($auth_block, true);

        if ($response !== "I LOVE YOU\n") {
            if ($response === "I HATE YOU\n") {
                return $this->throwError("Authentication failed");
            } else {
                return $this->throwError("Unrecognized authentication response: '$response'");
            }
        }
        return TRUE;
    }

    /**
     * pserver: Translate password to CVS format
     *
     * @param string Something to eat =)
     * @return string result
     */
    function scramble_pass($food) {

        $from = '!"%&'."'()*+,-./";
        $to   = 'x5mH'."lF@LCtJDW";

        $from .= '0123456789:;<=>?';
        $to   .= 'o4Kw1"RQ_ApVvnzi';

        $from .= 'ABCDEFGHIJKLMNO'.'PQRSTUVWXYZ_';
        $to   .= '9S+.f(Y&g-2*{[#'.'}76B|~;/\Gs8';

        $from .= "abcdefghijklmno"."pqrstuvwxyz";
        $to   .= "yuhedEIc?^]'%=0".":q Z,b<3!a>";

        return strtr($food, $from, $to);
    }

    /**
     * Initialize session. Send some required headers, request server info
     *
     * @return void
     */
    function _init() {
        // Provide information about supported responses
        // disabled for debug
        // $request = "Valid-responses ".implode(" ", $this->_net_cvs_responses)."\n";
        // $this->_send($request);

        // Ask and store information about server requests
        $request = "valid-requests \n";
        $response = $this->_send_wait_reply($request);

        $this->server_requests = str_replace("\nok\n","",split(" +", $response));

        // Follow protocol requirements
        $this->_send("UseUnchanged \n");
        // Crash CVSNT 2.0.51d
        // $response = $this->_send_wait_reply("noop \n");
        // $response = $this->_send_wait_reply("version \n");

        $this->_send("Root ".$this->_cvsroot."\n");
        $response = $this->_send_wait_reply("noop \n");
        $response = $this->_send_wait_reply("version \n");
        $this->server_version = preg_replace("!^M\s*(.*)\n$!", "$1", $response);
    }

    

    /***********************************************************
     * CVS. Response expected: no. Set a user variable to value
     *
     * @param string $variable
     * @param string $value
     * @return void
     */
    function setVariable($variable, $value) {
        $this->_send("Set $variable=$value \n");
    }

    /**
     * CVS. Response expected: yes. Get information about directories
     * with files server need to know about to proceed with update. Or
     * in other words - get list of directories, what make the module
     * contents on server.
     *
     * @param mixed $modules string or array
     * @return array directories
     */
    function expandModules($modules) {
        if (is_array( $modules )) {
           foreach ($modules as $m) {
               $this->_send("Argument $m\n");
           }
        } else {
           $this->_send("Argument $modules\n");
        }
        $response = $this->_send_wait_reply("expand-modules \n");
        $response = preg_replace("!Module-expansion !", "", $response, 1);
        $response = explode("\nModule-expansion ", $response);
        return $response;
    }

    /**
     * CVS. Response expected: yes. Checkout module from repository.
     *
     * Function makes an export of module files into module directory
     * specified by 'module' name.
     *
     * !!!CAUTION!!!: Directory with module name will be destroyed
     * 
     * TODO:
     * - real checkout with CVS information stored
     * - pass file/path/CVS info to special handler, which will decide
     *   where to store this information (pass amount of bytes to read
     *   and a handler where from to)
     *
     * @param string $module
     * @return void
     */
    function checkout($module) {
       if (file_exists($module)) {
           $this->throwError("Impossible to checkout: '$module' already exists in current directory");
           return;
       }
       $save_prefix = $module;
       $this->_send("Argument $module\n");
       $this->_send("Directory .\n".$this->_cvsroot."\n");
       $this->_send("co\n");
    }

    /**
     * CVS. Response expected: yes. Get a list of server modules
     *
     * TODO: add testcases (but how?)
     * TODO: -c against -s very different output
     * @return array with modules
     */
    function getModules() {
//        $this->_send("Argument -N\n");
        $this->_send("Argument -c\n");
        $this->_send("Directory .\n");
        $this->_send($this->_cvsroot."\n");
        echo $this->_send_wait_reply("co \n"); die();
        $response = explode("\n", $this->_send_wait_reply("co \n"));
 
        $modules = array();
        // valid characters for module name (like on cvs/php/net)
        $mn = "[\w-]+"; 
        foreach ($response as $r) {
            // upon match matches will be
            // 2nd component - mname
            // 3rd component - status
            // 4th component - directory
            // 5th component - array of aliases where 
            //   2nd in 5th component - is referenced module name
            // 
            // mname status directory &alias
            preg_match("!^M (($mn)\s+($mn)\s+($mn))?(\s+&.+)?$!", $r, $matches);
                                     // tests with different module files
            if ($matches[2]) {
                $module = array();
                $module['mname']     = $matches[2];
                $module['status']    = $matches[3];
                $module['directory'] = $matches[4];
            } else {
                $module = array_pop($modules);
            }

            if (isset($matches[5])) {
                preg_match_all("!&($mn)!", $matches[5], $refs);
                foreach ($refs[1] as $fs) {
                    $module['references'][] = $fs;
                }
            }

            $modules[] = $module;
        }

        return $modules;
    }

    // alpha
    function listFiles($directory = "", $module = "") {
        if ($module) {
            // expand module to get a path on server
        }
        if ($directory{0} != '/') {
            $directory = '/'.$directory;
        }
        $this->_send("Argument -l\n");
        $this->_send("Directory .\n");
        $this->_send($this->_cvsroot."$directory\n");
        $this->_send_wait_reply("log \n");
    }


    /**
     * Open connection to specified port using blocking Net_Socket
     *
     * @param string $server
     * @param integer $port
     */
    function _connect($server, $port) {
        $this->_socket = &new Net_Socket();
        $result = $this->_socket->connect($server, $port, false, 3);  // persistent, timeout);

        if (PEAR::isError($result)) {
            return $result;
        }
        return TRUE;
    }

    /**
     * Send command. Return TRUE or PEAR_Error
     */
    function _send($command) {
        if ($this->_debug) { echo preg_replace("!^.!m","C: $0",$command); } // FIXME : ?replace echo
        $result = $this->_socket->write( $command );
        if (PEAR::isError($result)) {
            return $result;
        }
        return TRUE;
    }

    /**
     * Send command and return reply or PEAR_Error
     * In raw mode function doesn't check if response is ended with '\nok\n' or '\nerror\n'
     * raw used for authentication
     */
    function &_send_wait_reply($command, $raw = false) {
        if ($this->_debug) { echo preg_replace("!^.!m","C: $0",$command); }

        $result = $this->_socket->write( $command );
        if (PEAR::isError($result)) {
            return $result;
        }

        $response = '';

        if ($raw) {  // 2048 is enough for auth
            $response = $this->_socket->read(2048);
        } else {
            while(!preg_match("!(ok|error)\n$!", $response)) {
/*                echo "Select dispatched \n";                            // d
                $stas = $this->_socket->select(NET_SOCKET_READ | NET_SOCKET_ERROR, 3);     // d
                echo "Select returned "; var_dump($stas); echo "\n";    // d
                $str = $this->_socket->read(2048);                      // d
                echo "Bytes read - "; echo strlen($str); echo "\n";     // d
                $response .= $str;
 /*/            $response .= $this->_socket->read(2048);
            }

            if ($this->_debug) { echo preg_replace("!^.!m","S: $0",$response); }

            if (preg_match("!error\n$!", $response)) {
                $this->throw_error($response);
            }
            $response = preg_replace("!\n?(ok|error)\n$!","", $response);
        }

        return $response;
    }

}


//$ns->setErrorHandling(PEAR_ERROR_DIE);

//$ns->login("pserver", 'guest', 'yyy');

//$ns->dsn_connect(":pserver::/wwewe");
//$ns->pserver_auth('techtonik', 'your_pass_here');

//$ns->_socket->write($auth_block);
//echo $ns->_socket->read(600);
/*
$req = "valid-requests
";

echo $ns->_socket->read(0);
print_r($ns->_socket->getStatus());
$ns->_socket->write($req);
print_r($ns->_socket->getStatus());
echo $ns->_socket->read(96);
print_r($ns->_socket->getStatus());
echo $ns->_socket->read(96);
print_r($ns->_socket->getStatus());
echo $ns->_socket->readAll();
print_r($ns->_socket->getStatus());
  */
